use std::time::Duration;

use rustc_abi::{FieldIdx, Size};

use crate::concurrency::init_once::{EvalContextExt as _, InitOnceStatus};
use crate::concurrency::sync::{AccessKind, FutexRef, SyncObj};
use crate::*;

#[derive(Clone)]
struct WindowsInitOnce {
    init_once: InitOnceRef,
}

impl SyncObj for WindowsInitOnce {
    fn on_access<'tcx>(&self, access_kind: AccessKind) -> InterpResult<'tcx> {
        if !self.init_once.queue_is_empty() {
            throw_ub_format!(
                "{access_kind} of `INIT_ONCE` is forbidden while the queue is non-empty"
            );
        }
        interp_ok(())
    }

    fn delete_on_write(&self) -> bool {
        true
    }
}

struct WindowsFutex {
    futex: FutexRef,
}

impl SyncObj for WindowsFutex {}

impl<'tcx> EvalContextExtPriv<'tcx> for crate::MiriInterpCx<'tcx> {}
trait EvalContextExtPriv<'tcx>: crate::MiriInterpCxExt<'tcx> {
    // Windows sync primitives are pointer sized.
    // We only use the first byte for the "init" flag.

    fn init_once_get_data<'a>(
        &'a mut self,
        init_once_ptr: &OpTy<'tcx>,
    ) -> InterpResult<'tcx, &'a WindowsInitOnce>
    where
        'tcx: 'a,
    {
        let this = self.eval_context_mut();

        let init_once =
            this.deref_pointer_as(init_once_ptr, this.windows_ty_layout("INIT_ONCE"))?;
        let init_offset = Size::ZERO;

        this.get_immovable_sync_with_static_init(
            &init_once,
            init_offset,
            /* uninit_val */ 0,
            /* init_val */ 1,
            |this| {
                let ptr_field = this.project_field(&init_once, FieldIdx::from_u32(0))?;
                let val = this.read_target_usize(&ptr_field)?;
                if val == 0 {
                    interp_ok(WindowsInitOnce { init_once: InitOnceRef::new() })
                } else {
                    throw_ub_format!("`INIT_ONCE` was not properly initialized at this location, or it got overwritten");
                }
            },
        )
    }

    /// Returns `true` if we were succssful, `false` if we would block.
    fn init_once_try_begin(
        &mut self,
        init_once_ref: &InitOnceRef,
        pending_place: &MPlaceTy<'tcx>,
        dest: &MPlaceTy<'tcx>,
    ) -> InterpResult<'tcx, bool> {
        let this = self.eval_context_mut();
        interp_ok(match init_once_ref.status() {
            InitOnceStatus::Uninitialized => {
                init_once_ref.begin();
                this.write_scalar(this.eval_windows("c", "TRUE"), pending_place)?;
                this.write_scalar(this.eval_windows("c", "TRUE"), dest)?;
                true
            }
            InitOnceStatus::Complete => {
                this.init_once_observe_completed(init_once_ref)?;
                this.write_scalar(this.eval_windows("c", "FALSE"), pending_place)?;
                this.write_scalar(this.eval_windows("c", "TRUE"), dest)?;
                true
            }
            InitOnceStatus::Begun => false,
        })
    }
}

impl<'tcx> EvalContextExt<'tcx> for crate::MiriInterpCx<'tcx> {}
#[allow(non_snake_case)]
pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
    fn InitOnceBeginInitialize(
        &mut self,
        init_once_op: &OpTy<'tcx>,
        flags_op: &OpTy<'tcx>,
        pending_op: &OpTy<'tcx>,
        context_op: &OpTy<'tcx>,
        dest: &MPlaceTy<'tcx>,
    ) -> InterpResult<'tcx> {
        let this = self.eval_context_mut();

        let init_once = this.init_once_get_data(init_once_op)?.init_once.clone();
        let flags = this.read_scalar(flags_op)?.to_u32()?;
        // PBOOL is int*
        let pending_place = this.deref_pointer_as(pending_op, this.machine.layouts.i32)?;
        let context = this.read_pointer(context_op)?;

        if flags != 0 {
            throw_unsup_format!("unsupported `dwFlags` {flags} in `InitOnceBeginInitialize`");
        }

        if !this.ptr_is_null(context)? {
            throw_unsup_format!("non-null `lpContext` in `InitOnceBeginInitialize`");
        }

        if this.init_once_try_begin(&init_once, &pending_place, dest)? {
            // Done!
            return interp_ok(());
        }

        // We have to block, and then try again when we are woken up.
        let dest = dest.clone();
        this.init_once_enqueue_and_block(
            init_once.clone(),
            callback!(
                @capture<'tcx> {
                    init_once: InitOnceRef,
                    pending_place: MPlaceTy<'tcx>,
                    dest: MPlaceTy<'tcx>,
                }
                |this, unblock: UnblockKind| {
                    assert_eq!(unblock, UnblockKind::Ready);
                    let ret = this.init_once_try_begin(&init_once, &pending_place, &dest)?;
                    assert!(ret, "we were woken up but init_once_try_begin still failed");
                    interp_ok(())
                }
            ),
        );
        interp_ok(())
    }

    fn InitOnceComplete(
        &mut self,
        init_once_op: &OpTy<'tcx>,
        flags_op: &OpTy<'tcx>,
        context_op: &OpTy<'tcx>,
    ) -> InterpResult<'tcx, Scalar> {
        let this = self.eval_context_mut();

        let init_once = this.init_once_get_data(init_once_op)?.init_once.clone();
        let flags = this.read_scalar(flags_op)?.to_u32()?;
        let context = this.read_pointer(context_op)?;

        let success = if flags == 0 {
            true
        } else if flags == this.eval_windows_u32("c", "INIT_ONCE_INIT_FAILED") {
            false
        } else {
            throw_unsup_format!("unsupported `dwFlags` {flags} in `InitOnceBeginInitialize`");
        };

        if !this.ptr_is_null(context)? {
            throw_unsup_format!("non-null `lpContext` in `InitOnceBeginInitialize`");
        }

        if init_once.status() != InitOnceStatus::Begun {
            // The docs do not say anything about this case, but it seems better to not allow it.
            throw_ub_format!(
                "calling InitOnceComplete on a one time initialization that has not begun or is already completed"
            );
        }

        if success {
            this.init_once_complete(&init_once)?;
        } else {
            this.init_once_fail(&init_once)?;
        }

        interp_ok(this.eval_windows("c", "TRUE"))
    }

    fn WaitOnAddress(
        &mut self,
        ptr_op: &OpTy<'tcx>,
        compare_op: &OpTy<'tcx>,
        size_op: &OpTy<'tcx>,
        timeout_op: &OpTy<'tcx>,
        dest: &MPlaceTy<'tcx>,
    ) -> InterpResult<'tcx> {
        let this = self.eval_context_mut();

        let ptr = this.read_pointer(ptr_op)?;
        let compare = this.read_pointer(compare_op)?;
        let size = this.read_target_usize(size_op)?;
        let timeout_ms = this.read_scalar(timeout_op)?.to_u32()?;

        if size > 8 || !size.is_power_of_two() {
            let invalid_param = this.eval_windows("c", "ERROR_INVALID_PARAMETER");
            this.set_last_error(invalid_param)?;
            this.write_scalar(Scalar::from_i32(0), dest)?;
            return interp_ok(());
        };
        let size = Size::from_bytes(size);

        let timeout = if timeout_ms == this.eval_windows_u32("c", "INFINITE") {
            None
        } else {
            let duration = Duration::from_millis(timeout_ms.into());
            Some((TimeoutClock::Monotonic, TimeoutAnchor::Relative, duration))
        };

        // See the Linux futex implementation for why this fence exists.
        this.atomic_fence(AtomicFenceOrd::SeqCst)?;

        let layout = this.machine.layouts.uint(size).unwrap();
        let futex_val =
            this.read_scalar_atomic(&this.ptr_to_mplace(ptr, layout), AtomicReadOrd::Acquire)?;
        let compare_val = this.read_scalar(&this.ptr_to_mplace(compare, layout))?;

        if futex_val == compare_val {
            // If the values are the same, we have to block.

            // This cannot fail since we already did an atomic acquire read on that pointer.
            let futex_ref = this
                .get_sync_or_init(ptr, |_| WindowsFutex { futex: Default::default() })
                .unwrap()
                .futex
                .clone();

            let dest = dest.clone();
            this.futex_wait(
                futex_ref,
                u32::MAX, // bitset
                timeout,
                callback!(
                    @capture<'tcx> {
                        dest: MPlaceTy<'tcx>
                    }
                    |this, unblock: UnblockKind| {
                        match unblock {
                            UnblockKind::Ready => {
                                this.write_int(1, &dest)
                            }
                            UnblockKind::TimedOut => {
                                this.set_last_error(IoError::WindowsError("ERROR_TIMEOUT"))?;
                                this.write_int(0, &dest)
                            }
                        }
                    }
                ),
            );
        }

        this.write_scalar(Scalar::from_i32(1), dest)?;

        interp_ok(())
    }

    fn WakeByAddressSingle(&mut self, ptr_op: &OpTy<'tcx>) -> InterpResult<'tcx> {
        let this = self.eval_context_mut();

        let ptr = this.read_pointer(ptr_op)?;

        // See the Linux futex implementation for why this fence exists.
        this.atomic_fence(AtomicFenceOrd::SeqCst)?;

        let Some(futex_ref) =
            this.get_sync_or_init(ptr, |_| WindowsFutex { futex: Default::default() })
        else {
            // Seems like this cannot return an error, so we just wake nobody.
            return interp_ok(());
        };
        let futex_ref = futex_ref.futex.clone();

        this.futex_wake(&futex_ref, u32::MAX, 1)?;

        interp_ok(())
    }
    fn WakeByAddressAll(&mut self, ptr_op: &OpTy<'tcx>) -> InterpResult<'tcx> {
        let this = self.eval_context_mut();

        let ptr = this.read_pointer(ptr_op)?;

        // See the Linux futex implementation for why this fence exists.
        this.atomic_fence(AtomicFenceOrd::SeqCst)?;

        let Some(futex_ref) =
            this.get_sync_or_init(ptr, |_| WindowsFutex { futex: Default::default() })
        else {
            // Seems like this cannot return an error, so we just wake nobody.
            return interp_ok(());
        };
        let futex_ref = futex_ref.futex.clone();

        this.futex_wake(&futex_ref, u32::MAX, usize::MAX)?;

        interp_ok(())
    }
}
